#include <iostream>
#include <fstream>
#include <string>
#include <GL\glut.h>
#include "Camera.h"
#include "imageloader.h"

using namespace std;

// Functions Declarations

void Timer(int);
Vec3f getNormal(Vec3f, Vec3f, Vec3f);
void drawScene();

// Global Variable Declarations

string fileName = "PainDeSucre.dem"; // The relative path to the file to use to build the elevation matrix from
int A[41][34];						// The matrix of elevations of the Pain de Sucre mountain created from the .dem file
int count = 0;						// total amount of vertices for the mountain which is also the amount of elevation points from the .dem file

float longitudeSpacing = 16.4;	// east-west spacing between elevation data in meters
float latitudeSpacing = 23.2;	// north-south spacing between elevation data in meters

int fovy = 45;		// Field of View Y of the Perspective
float ratio = 1;	// Ratio of the Perspective
float nPlane = 0.1;	// Near clipping plane
float fPlane = 2000; // Far clipping plane

int cameraSpeed = 5;	// Speed at which the camera moves forward and backward
int zoomSpeed = 2;		// Speed at which the camera zooms in and out

Camera* cam;
Camera* cam1;
Camera* cam2;
Camera* cam3;

bool spin = false;		// when true, mountains spins
GLfloat spinAngle = 0;	// current angle at which the mountain is "spinned"

// Texture
static GLuint texName;
Image * image;
string textFile = "f2.bmp"; // Texture file to load on mountain (must be power of 2)
bool displayTexture = true;

// Lighting
bool displayLighting = true;

// Fog
bool displayFog = true;

// Viewports
int windowWidth;
int windowHeight;
bool dividedViewPort = true;

/*
* Draws a 41X34 grid of triangles.
* The spacing on the X axis is determined by the longitude spacing
* and on the Z axis by the latitude spacing. The Y coordinates are taken
* from the elevation data of the mountain.

* This results in an accurate representation of the mountain visually.
*/
void drawMountain()
{
	// Lighting
	if(displayLighting)
	{
		//GLfloat mcolor[] = {0.8f, 1.0f, 0.8f, 1.0f};
		//glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mcolor);

		// Enable color tracking to use the vertex color as material reflectiveness
		glEnable(GL_COLOR_MATERIAL);
		glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
	}
	else
	{
		glDisable(GL_COLOR_MATERIAL);
	}

	// Since we have no lighting model, the mountain looks flat, by drawing the mountain
	// in wireFrame it helps us properly see the elevation of the mountain.
	//glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );	// Required so that we can better see the elevation in the mountain.

	int c1 = 0; // red color
	int c2 = 0; // green color

	Vec3f a; // 1st vertex of triangle
	Vec3f b; // 2nd vertex of triangle
	Vec3f c; // 3rd vertex of triangle

	// The following loops are used to enclose the mountain surface
	glColor3f(0.349,0.22,0);
	glBegin(GL_TRIANGLE_STRIP);
	for(int row = 0; row != 41-1; ++row)
	{
		a = Vec3f(0, A[row][0], row*latitudeSpacing);
		b = Vec3f(0, 0, row*latitudeSpacing);
		c = Vec3f(0, A[row+1][0], (row+1)*latitudeSpacing);

		Vec3f n = getNormal(a,b,c);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
		glVertex3f(b[0], b[1], b[2]);
		glVertex3f(c[0], c[1], c[2]);

		a = Vec3f(0, 0, (row+1)*latitudeSpacing);

		n = getNormal(c,b,a);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
	}
	glEnd();
	
	glBegin(GL_TRIANGLE_STRIP);
	for(int col = 0; col != 34-1; ++col)
	{
		a = Vec3f(col*longitudeSpacing, A[0][col], 0);
		b = Vec3f((col+1)*longitudeSpacing, A[0][col+1], 0);
		c = Vec3f(col*longitudeSpacing, 0, 0);

		Vec3f n = getNormal(a,b,c);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
		glVertex3f(b[0], b[1], b[2]);
		glVertex3f(c[0], c[1], c[2]);

		a = Vec3f((col+1)*longitudeSpacing, 0, 0);

		n = getNormal(c,b,a);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
	}
	glEnd();

	glBegin(GL_TRIANGLE_STRIP);
	for(int row = 0; row != 41-1; ++row)
	{
		a = Vec3f(33*longitudeSpacing, A[row][33], row*latitudeSpacing);
		b = Vec3f(33*longitudeSpacing, A[row+1][33], (row+1)*latitudeSpacing);
		c = Vec3f(33*longitudeSpacing, 0, row*latitudeSpacing);

		Vec3f n = getNormal(a,b,c);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
		glVertex3f(b[0], b[1], b[2]);
		glVertex3f(c[0], c[1], c[2]);

		a = Vec3f(33*longitudeSpacing, 0, (row+1)*latitudeSpacing);

		n = getNormal(c,b,a);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
	}
	glEnd();
	
	glBegin(GL_TRIANGLE_STRIP);
	for(int col = 0; col != 34-1; ++col)
	{
		a = Vec3f(col*longitudeSpacing, A[40][col], 40*latitudeSpacing);
		b = Vec3f(col*longitudeSpacing, 0, 40*latitudeSpacing);
		c = Vec3f((col+1)*longitudeSpacing, A[40][col+1], 40*latitudeSpacing);

		Vec3f n = getNormal(a,b,c);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
		glVertex3f(b[0], b[1], b[2]);
		glVertex3f(c[0], c[1], c[2]);

		a = Vec3f((col+1)*longitudeSpacing, 0, 40*latitudeSpacing);

		n = getNormal(c,b,a);
		glNormal3f(n[0], n[1], n[2]);

		glVertex3f(a[0], a[1], a[2]);
	}
	glEnd();
	
	// Enable Texture Mapping
	if(displayTexture)
		glEnable(GL_TEXTURE_2D);

	if(displayLighting)
		glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	else
		glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
	glBindTexture(GL_TEXTURE_2D, texName);
	// Texture spacing
	float ws = longitudeSpacing / (34*longitudeSpacing);
	float hs = latitudeSpacing / (41*latitudeSpacing);

	// Multiply Mountain Diffuse with this color
	if(displayLighting)
		glColor3f(1,1,1);

	// Iterating on rows and columns of the elevation matrix A we will
	// draw the triangles of the grid. We must not loop on every row and col, because
	// we are drawing 3 vertices on each iteration, thus we stop on row - 1 and col - 1.
	// One triangle strip is used per row, this saves on computational power, because it reduces
	// the amount of calls to glVertex3f();
	// NOTE: The mountain is drawn with triangles facing UP (0,1,0) using CCW mode.
	for(int row = 0; row != 41-1; ++row)
	{
		c1 = c1 + 2 % 255; // increment color each row

		glBegin(GL_TRIANGLE_STRIP);		// Begin triangle strip on each row
			for(int col = 0; col != 34-1+1; ++col)
			{
				c2 = c2 + 2 % 255; // increment color each col

				if(!displayLighting)
				{
					// Series of if condition that identifies the side of the mountain
					// and sets it to a specific color as required by the assignment.
					if(col == 34-1) glColor3f(0,1,0);
					else if(row == 0) glColor3f(1,1,0);
					else if(row == 41-2) glColor3f(0,1,1);
					else if(col == 0) glColor3f(0,0,1);
					else glColor3b(c1,c2,255);
				}

				// Special case for first column of each row. This is the start of each triangle
				// strip, and therefore we must specify all vertix of the intial triangle.
				if(col == 0)
				{
					a = Vec3f(col*longitudeSpacing, A[row][col], row*latitudeSpacing);
					b = Vec3f(col*longitudeSpacing, A[row+1][col], (row+1)*latitudeSpacing);
					c = Vec3f((col+1)*longitudeSpacing, A[row][col+1], row*latitudeSpacing);

					Vec3f n = getNormal(a,b,c);
					glNormal3f(n[0], n[1], n[2]);

					glTexCoord2f(col * ws, row * hs); glVertex3f(a[0], a[1], a[2]);
					glTexCoord2f(col * ws, (row+1) * hs); glVertex3f(b[0], b[1], b[2]);
					glTexCoord2f((col+1) * ws, row * hs); glVertex3f(c[0], c[1], c[2]);
				}
				// Special case for the second column of each row. Since we have already specified both
				// vertex of this triangle on col == 0, we need only add one extra vertex
				else if(col == 1)
				{
					a = Vec3f(col*longitudeSpacing, A[row+1][col], (row+1)*latitudeSpacing);

					Vec3f n = getNormal(c,b,a);
					glNormal3f(n[0], n[1], n[2]);

					glTexCoord2f(col * ws, (row+1) * hs); glVertex3f(a[0], a[1], a[2]);
				}
				// Normal case, for all remaining columns. One vertex was defined in the last iteration,
				// so two more vetrices must be defined to draw a triangle.
				else
				{
					b = Vec3f(col*longitudeSpacing, A[row][col], row*latitudeSpacing);
					c = Vec3f(col*longitudeSpacing, A[row+1][col], (row+1)*latitudeSpacing);

					Vec3f n = getNormal(b,a,c);
					glNormal3f(n[0], n[1], n[2]);

					glTexCoord2f(col * ws, row * hs); glVertex3f(b[0], b[1], b[2]);
					glTexCoord2f(col * ws, (row+1) * hs); glVertex3f(c[0], c[1], c[2]);
				}
			}
		glEnd();	// End triangle strip at the end of each row
	}

	// Disable Texture Mapping
	glDisable(GL_TEXTURE_2D);

	//glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );	// Reset PolygonMode to default
}

void drawAxis(float lengthOfAxis, float lineWidth)
{
	glLineWidth(lineWidth);

	glBegin(GL_LINES);

	glColor3f(1,0,0); // X axis is red.
	glVertex3f(-lengthOfAxis,0,0);
	glVertex3f(lengthOfAxis,0,0); 

	glColor3f(0,1,0); // Y axis is green.
	glVertex3f(0,-lengthOfAxis,0);
	glVertex3f(0,lengthOfAxis,0);

	glColor3f(0,0,1); // z axis is blue.
	glVertex3f(0,0,-lengthOfAxis);
	glVertex3f(0,0,lengthOfAxis); 

	glEnd();

	glLineWidth(1.0);
}

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);		// Clear buffers, we don't want stuff to draw over each other

	if(!dividedViewPort)
	{
		glViewport(0, 0, windowWidth, windowHeight); // Viewport fills window
		cam->update();
		drawScene();
	}
	else
	{
		// Bottom left
		glViewport(0, 0, windowWidth/2, windowHeight/2);
		cam->update();
		drawScene();

		// Bottom right
		glViewport(windowWidth/2, 0, windowWidth/2, windowHeight/2);
		cam1->lookAt(400,550,-500, 0,0,0);
		cam1->update();
		drawScene();

		// Top Left
		glViewport(0, windowHeight/2, windowWidth/2, windowHeight/2);
		cam2->lookAt(-200,550,400, 800,100,-800);
		cam2->update();
		drawScene();

		// Top Right
		glViewport(windowWidth/2, windowHeight/2, windowWidth/2, windowHeight/2);
		cam3->lookAt(400,1500,400, 0,300,0);
		cam3->moveRight(600);
		cam3->moveDown(160);
		cam3->update();
		drawScene();
	}

	glutSwapBuffers();
}

void drawScene()
{
	glPushMatrix();
	glRotatef(90,0,1,0);	// Rotate mountain so that it shows in the positive X, negative Z, positive Y octant

	// Spin on Y axis at highest peak, which is found at A[25][13]
	glTranslatef((13*longitudeSpacing), 0, (25*latitudeSpacing));
	glRotatef(spinAngle, 0,1,0);
	glTranslatef(-(13*longitudeSpacing), 0, -(25*latitudeSpacing));

	drawMountain();
	glPopMatrix();

	if(displayFog)
	{
		glDisable(GL_FOG);
		drawAxis(10000, 3.0);
		glEnable(GL_FOG);
	}
	else
	{
		drawAxis(10000, 3.0);
	}
}

void reshape(int w, int h)
{
	windowWidth = w;
	windowHeight = h;

	if (h == 0) h = 1;		// Protect against division by zero, in case height = 0
	ratio = (float) w/h;	// Get aspect rato based on w and h of window

	// Set the PROJECTION matrix so that it reflects the new ratio
	glMatrixMode(GL_PROJECTION);

	glLoadIdentity();
	gluPerspective(fovy,ratio,nPlane,fPlane); // projection will have perspective

	glMatrixMode(GL_MODELVIEW); // Reset default MatrixMode
}

/*
* Apply changes to fovy to the PROJECTION matrix
*/
void resetZoom()
{
	glMatrixMode(GL_PROJECTION);

	glLoadIdentity();
	gluPerspective(fovy,ratio,nPlane,fPlane);

	glMatrixMode(GL_MODELVIEW);
}

void keyboard(unsigned char key, int x, int y)
{
	switch(key)
	{
		case '+':	// zoom in
			fovy -= cameraSpeed;
			resetZoom();
			break;
		case '-':	// zoom out
			fovy += cameraSpeed;
			resetZoom();
			break;
		case 'f':	// forward
			cam->moveForward(cameraSpeed);
			break;
		case 'b':	// backward
			cam->moveBackward(cameraSpeed);
			break;
		case 'a':
			cam->moveLeft(cameraSpeed);
			break;
		case 'd':
			cam->moveRight(cameraSpeed);
			break;
		case 's':	// set spin on/off
			spin = !spin;
			glutTimerFunc(35,Timer,0);
			break;
		case 'l':
			cam->rollLeft(cameraSpeed);
			break;
		case 'r':
			cam->rollRight(cameraSpeed);
			break;
		case 't':
			displayTexture = !displayTexture;
			break;
		case 'L':
			displayLighting = !displayLighting;
			if(displayLighting)
				glEnable(GL_LIGHTING);
			else
				glDisable(GL_LIGHTING);
			break;
		case 'F':
			displayFog = !displayFog;
			if(displayFog)
				glEnable(GL_FOG);
			else
				glDisable(GL_FOG);
			break;
		case 'v':
			dividedViewPort = !dividedViewPort;
			break;
	}

	glutPostRedisplay();
}

void specialKeyboard(int key, int x, int y)
{
	switch(key)
	{
		case GLUT_KEY_UP:
			cam->pitchUp(cameraSpeed);
			break;
		case GLUT_KEY_DOWN:
			cam->pitchDown(cameraSpeed);
			break;
		case GLUT_KEY_LEFT:
			cam->yawLeft(cameraSpeed);
			break;
		case GLUT_KEY_RIGHT:
			cam->yawRight(cameraSpeed);
			break;
		case GLUT_KEY_PAGE_UP:
			cam->moveUp(cameraSpeed);
			break;
		case GLUT_KEY_PAGE_DOWN:
			cam->moveDown(cameraSpeed);
			break;
	}

	glutPostRedisplay();
}

/*
* Used to animate the spin of the mountain based on time
*/
void Timer(int value)
{
	if(spin)
	{
		spinAngle = spinAngle + cameraSpeed;
		glutPostRedisplay();

		glutTimerFunc(35,Timer,0);	// Must re-add event listener, because it automatically removes itself at each Timer event
	}
}

/*
* Find the normal of a polygon and returns it
*/
Vec3f getNormal(Vec3f v1, Vec3f v2, Vec3f v3)
{
	Vec3f ret;

	ret = (v1 - v2).cross(v2 - v3);

    // normalize
	ret = ret.normalize();

	return ret;
}

void main(int argc, char **argv)
{
	// Read in the elevation data from the file
	ifstream fin;
	fin.open(fileName.c_str());
	if (!fin)
	{
		cerr << "Failed to open " << fileName << endl;
		return;
	}

	int elevation = 0;
	int row = 0;
	int col = 0;
	while (fin >> elevation)
	{
		row = count / 34;
		col = count % 34;

		A[row][col] = elevation;

		count++;
	}

	// Start glut
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	glutInitWindowSize(500,500);

	glutCreateWindow("Didier Arslanian - Assignment 2");

	//glClearColor(0.8, 0.867, 1, 0); // sets the color that will be used to clear the color buffers
	glClearColor(0.529, 0.807, 0.98, 0); // sets the color that will be used to clear the color buffers

	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);

	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);

	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	glutKeyboardFunc(keyboard);
	glutSpecialFunc(specialKeyboard);

	// Camera set-up
	cam = new Camera;
	cam1 = new Camera;
	cam2 = new Camera;
	cam3 = new Camera;

	cam->moveTo(0, 550, 0);
	cam->lookAt(580, 407, -213.2);

	//cam->lookAt(0,550,0, 580,407,-213.2);

	// Texture set-up
	image = loadBMP(textFile.c_str());
	glGenTextures(1, &texName);
	glBindTexture(GL_TEXTURE_2D, texName);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	//glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->pixels);
	gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB, image->width, image->height, GL_RGB, GL_UNSIGNED_BYTE, image->pixels); // enable mipmapping

	// Light set-up
	glEnable(GL_LIGHTING);
	glShadeModel(GL_SMOOTH);

	GLfloat global_ambient[] = { 0.5f, 0.5f, 0.5f, 1.0f };
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, global_ambient);

	// Create light components
	GLfloat ambientLight[] = { 0.2f, 0.2f, 0.3f, 1.0f};
	GLfloat diffuseLight[] = { 0.8f, 0.8f, 0.1f, 1.0f};
	GLfloat specularLight[] = { 0.2f, 0.2f, 0.3f, 1.0f};
	GLfloat emissiveLight[] = {1.0f, 1.0f, 1.0f , 1.0f};
	GLfloat position[] = {0, 800, 0, 0.5};

	// Assign created components to GL_LIGHT0
	glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
	glLightfv(GL_LIGHT0, GL_SPECULAR, specularLight);
	glLightfv(GL_LIGHT0, GL_POSITION, position);

	glEnable(GL_LIGHT0);

	// Fog set-up
	GLfloat density = 0.0009;
	GLfloat fogColor[4] = {0.4, 0.4, 0.4, 1.0};
	glEnable(GL_FOG);
	glFogi(GL_FOG_MODE, GL_EXP2);
	glFogfv(GL_FOG_COLOR, fogColor);
	glFogf(GL_FOG_DENSITY, density);

	glutMainLoop();
}